/*
 * FILE:        WindowsLiveLogin.cs
 *                                                                      
 * DESCRIPTION: Sample implementation of Auth Lite protocol in C#.  This code library allows one to quickly and easily
 *              integrate AuthLite into your website.  This code does not need to be changed or modified for the hands on lab.
 *
 * HISTORY:     16 April 2007 - Mix '07 Version, Subject To Change.
 *
 * Copyright <cp> 2007 Microsoft Corporation.  All Rights Reserved.
 * 
 * 
 */

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Specialized;
using System.Collections;
using System.Web;
using System.Web.Configuration;
using System.Security.Cryptography;
using System.IO;
using System.Net;
using System.Reflection;
using System.Drawing;

namespace WindowsLiveAuthLite
{
    /// <summary>
    /// Implementation of basic methods for Auth Lite support.
    /// </summary>
    public class WindowsLiveLogin
    {
        /// <summary>
        /// Stub implementation for logging debug output.  Run a tool
        /// such as 'dbmon' to see the output.
        /// </summary>
        static void debug(string msg)
        {
            System.Diagnostics.Debug.WriteLine(msg);
            System.Diagnostics.Debug.Flush();
        }

        /// <summary>
        /// Holds the user information after a successful login.
        /// </summary>
        /// <list type="table">
        ///  <item><term>timestamp</term> <description>is the time as
        ///  obtained from the SSO token.</description></item>
        ///
        ///  <item><term>id</term> <description>is the pairwise unique
        ///  ID for the user.</description></item>
        ///
        ///  <item><term>context</term> <description>is the
        ///  application context that was originally passed to the
        ///  login request, if any.</description>
        ///
        ///  <item><term>token</term> <description>is the encrypted
        ///  AuthLite token containing the UID. This can be cached in
        ///  a cookie and the UID can be retrieved by calling the
        ///  processToken method.</description>
        /// </list>
        public class User
        {
            string timestamp;
            string id;
            string context;
            string token;

            public User(string timestamp, string id, string context, string token)
            {
                this.timestamp = timestamp;
                this.id = id;
                this.context = context;
                this.token = token;
            }

            public bool IsSane()
            {
                if (Timestamp == null || Id == null)
                    return false;

                try
                {
                    Convert.ToInt32(Timestamp);
                }
                catch (Exception)
                {
                    return false;
                }

                Regex re = new Regex("^\\w+$");
                if (!re.IsMatch(Id))
                    return false;

                return true;
            }

            public string Id { get { return id; } }
            public string Timestamp { get { return timestamp; } }
            public string Context { get { return context; } }
            public string Token { get { return token; } }
        }

        /// <summary>
        /// Initialize the WindowsLiveLogin module.  In the Mix version
        /// we read the App ID and Secret from the web.config file, if
        /// present.  If desired, you can also set this information by
        /// setting the AppId and Secret properties.
        /// </summary>
        public WindowsLiveLogin()
        {
            NameValueCollection appSettings = WebConfigurationManager.AppSettings;
            if (appSettings != null)
            {
                string appId = appSettings["appid"];
                if (appId != null || appId.Length == 0)
                    AppId = appSettings["appid"];
                string appKey = appSettings["secret"];
                if (appKey != null || appId.Length == 0)
                    Secret = appSettings["secret"];
            }
        }

        string appId;

        /// <summary>
        /// Get or set the application id.
        /// </summary>
        public string AppId
        {
            get
            {
                if (appId == null || appId.Length == 0)
                    throw new InvalidOperationException("Error: AppId: App Id was not set.  Aborting.");
                return appId;
            }
            set
            {
                if (value == null || value.Length == 0)
                    throw new ArgumentNullException("value");
                appId = value;
            }
        }

        byte[] cryptKey;
        byte[] signKey;

        /// <summary>
        /// Set the application secret.
        /// </summary>
        public string Secret
        {
            set
            {
                if (value == null || value.Length == 0)
                    throw new ArgumentNullException("value");
                using (HashAlgorithm sha256Algorithm = HashAlgorithm.Create("SHA256"))
                {
                    cryptKey = derive(sha256Algorithm, value, "ENCRYPTION");
                    signKey = derive(sha256Algorithm, value, "SIGNATURE");
                }
            }

            get
            {
                return null;
            }
        }

        /// <summary>
        /// Processes the login response from Windows Live Login.
        /// </summary>
        ///
        /// <param name="query">Contains the pre-processed POST query
        /// such as that returned by HttpRequest.Form</param>
        /// 
        /// <returns>The method returns a User object on successful
        /// login, null otherwise.</returns>
        public User ProcessLogin(NameValueCollection query)
        {
            if (query == null)
                throw new ArgumentNullException("query");
            string action = query["action"];
            if (action != "login")
            {
                debug("Warning: ProcessLogin: query action ignored: " + action);
                return null;
            }
            string token = query["stoken"];
            string context = query["appctx"];
            if (context != null)
                context = HttpUtility.UrlDecode(context);
            return ProcessToken(token, context);
        }

        /// <summary>
        /// Decodes and validates an Auth Lite token.  Returns a User object on
        /// success.  If a context is passed in, it will be returned
        /// as the context field in the User object.  
        /// </summary>
        public User ProcessToken(string token, string context)
        {
            if (token == null || token.Length == 0)
            {
                debug("Error: ProcessToken: Invalid token.");
                return null;
            }

            string stoken = DecodeToken(token);
            if (stoken == null || stoken.Length == 0)
            {
                debug("Error: ProcessToken: Failed to decode token: " + token);
                return null;
            }

            stoken = ValidateToken(stoken);
            if (stoken == null || stoken.Length == 0)
            {
                debug("Error: ProcessToken: Failed to validate token: " + token);
                return null;
            }

            NameValueCollection parsedToken = HttpUtility.ParseQueryString(stoken);
            if (parsedToken == null || parsedToken.Count < 3)
            {
                debug("Error: ProcessToken: Failed to parse token: " + token);
                return null;
            }

            string appId = parsedToken["appid"];
            if (appId != AppId)
            {
                debug("Error: processToken: App ID in token did not match ours: " + appId +
                      ", " + AppId);
                return null;
            }

            User user = new User(parsedToken["ts"], parsedToken["uid"], context, token);
            if (user.IsSane())
                return user;
            debug("Error: processToken: Contents of token considered invalid: " + token);
            return null;
        }

        /// <summary>
        /// Decode the given string.  Returns null on failure.
        /// </summary>
        ///
        /// <list type="number">
        /// <item>First, the string is URL unescaped and base64 decoded.</item>
        /// <item>Second, the IV is extracted from the first 16 bytes of the string.</item>
        /// <item>Finally, the string is decrypted using the encryption key.</item>
        /// </list>
        public string DecodeToken(string token)
        {
            if (cryptKey == null || cryptKey.Length == 0)
                throw new InvalidOperationException("Error: decodeToken: App Key was not set. Aborting.");

            const int ivLength = 16;
            byte[] ivAndEncryptedValue = u64(token);

            if ((ivAndEncryptedValue == null) ||
                (ivAndEncryptedValue.Length <= ivLength) ||
                ((ivAndEncryptedValue.Length % 16) != 0))
            {
                debug("Error: DecodeToken: Attempted to decode invalid token.");
                return null;
            }

            Rijndael aesAlg = null;
            MemoryStream memStream = null;
            CryptoStream cStream = null;
            StreamReader sReader = null;
            string decodedValue = null;

            try
            {
                aesAlg = new RijndaelManaged();
                aesAlg.KeySize = 128;
                aesAlg.Key = cryptKey;
                aesAlg.Padding = PaddingMode.PKCS7;
                memStream = new MemoryStream(ivAndEncryptedValue);
                byte[] iv = new byte[ivLength];
                memStream.Read(iv, 0, ivLength);
                aesAlg.IV = iv;
                cStream = new CryptoStream(memStream, aesAlg.CreateDecryptor(), CryptoStreamMode.Read);
                sReader = new StreamReader(cStream, Encoding.ASCII);
                decodedValue = sReader.ReadToEnd();
            }
            catch (Exception e)
            {
                debug("Error: DecodeToken: Decryption failed: " + e.Message);
            }
            finally
            {
                if (sReader != null) sReader.Close();
                if (cStream != null) cStream.Close();
                if (memStream != null) memStream.Close();
                if (aesAlg != null) aesAlg.Clear();
            }

            return decodedValue;
        }

        /// <summary>
        /// Create a signature for the given string using the signature key.
        /// </summary>
        public byte[] SignToken(string token)
        {
            if (signKey == null || signKey.Length == 0)
                throw new ArgumentNullException("token");

            using (HashAlgorithm hmacSha256Algorithm = new HMACSHA256(signKey))
            {
                byte[] data = Encoding.Default.GetBytes(token);
                byte[] hash = hmacSha256Algorithm.ComputeHash(data);
                return hash;
            }
        }

        /// <summary>
        /// Extracts the signature from the token and validates it.
        /// </summary>
        public string ValidateToken(string token)
        {
            if (token == null || token.Length == 0)
            {
                debug("Error: ValidateToken: Invalid token.");
                return null;
            }

            string[] s = { "&sig=" };
            string[] bodyAndSig = token.Split(s, StringSplitOptions.None);

            if (bodyAndSig.Length != 2)
            {
                debug("Error: ValidateToken: Invalid token: " + token);
                return null;
            }

            byte[] sig = u64(bodyAndSig[1]);
            byte[] sig2 = SignToken(bodyAndSig[0]);

            if (sig.Length == sig2.Length)
            {
                for (int i = 0; i < sig.Length; i++)
                {
                    if (sig[i] != sig2[i])
                        goto badSig;
                }
                return token;
            }

        badSig:
            debug("Error: validateToken: Signature did not match.");
            return null;
        }

        /// <summary>
        /// Hardcoded clear cookie method for Mix '07.  All we need to
        /// do is return a GIF as response, to indicate to the login
        /// server that the clear cookie operation was successful.
        /// </summary>
        public void GetClearCookieResponse(out string type, out byte[] content)
        {
            // Base64 encoded GIF file.  This can be any valid GIF data.
            // We're hardcoding this here for convenience.
            string nullGif = @"
R0lGODlhAQABAOcAAP//////zP//mf//Zv//M///AP/M///MzP/Mmf/MZv/MM//MAP+Z//+ZzP+Z
mf+ZZv+ZM/+ZAP9m//9mzP9mmf9mZv9mM/9mAP8z//8zzP8zmf8zZv8zM/8zAP8A//8AzP8Amf8A
Zv8AM/8AAMz//8z/zMz/mcz/Zsz/M8z/AMzM/8zMzMzMmczMZszMM8zMAMyZ/8yZzMyZmcyZZsyZ
M8yZAMxm/8xmzMxmmcxmZsxmM8xmAMwz/8wzzMwzmcwzZswzM8wzAMwA/8wAzMwAmcwAZswAM8wA
AJn//5n/zJn/mZn/Zpn/M5n/AJnM/5nMzJnMmZnMZpnMM5nMAJmZ/5mZzJmZmZmZZpmZM5mZAJlm
/5lmzJlmmZlmZplmM5lmAJkz/5kzzJkzmZkzZpkzM5kzAJkA/5kAzJkAmZkAZpkAM5kAAGb//2b/
zGb/mWb/Zmb/M2b/AGbM/2bMzGbMmWbMZmbMM2bMAGaZ/2aZzGaZmWaZZmaZM2aZAGZm/2ZmzGZm
mWZmZmZmM2ZmAGYz/2YzzGYzmWYzZmYzM2YzAGYA/2YAzGYAmWYAZmYAM2YAADP//zP/zDP/mTP/
ZjP/MzP/ADPM/zPMzDPMmTPMZjPMMzPMADOZ/zOZzDOZmTOZZjOZMzOZADNm/zNmzDNmmTNmZjNm
MzNmADMz/zMzzDMzmTMzZjMzMzMzADMA/zMAzDMAmTMAZjMAMzMAAAD//wD/zAD/mQD/ZgD/MwD/
AADM/wDMzADMmQDMZgDMMwDMAACZ/wCZzACZmQCZZgCZMwCZAABm/wBmzABmmQBmZgBmMwBmAAAz
/wAzzAAzmQAzZgAzMwAzAAAA/wAAzAAAmQAAZgAAMwAAAP//////////////////////////////
////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////yH+HChjKSAxOTk5IE1pdGhy
YW5kaXIgUmVzZWFyY2gALAAAAAABAAEAAAgEAI8FBAA7";
            content = Convert.FromBase64String(nullGif);
            type = "image/gif";
        }

        /// <summary>
        /// Derive the key given secret and nonce as described in the
        /// SDK documentation.
        /// </summary>
        static byte[] derive(HashAlgorithm hashAlgorithm, string secret, string nonce)
        {
            const int keyLength = 16;
            byte[] data = Encoding.Default.GetBytes(nonce + secret);
            byte[] hashOutput = hashAlgorithm.ComputeHash(data);
            byte[] byteKey = new byte[keyLength];
            Array.Copy(hashOutput, byteKey, keyLength);
            return byteKey;
        }

        /// <summary>
        /// URL unescape and Base64 decode a string.
        /// </summary>
        static byte[] u64(string s)
        {
            byte[] b = null;
            if (s == null)
                return b;
            s = HttpUtility.UrlDecode(s);
            try
            {
                b = Convert.FromBase64String(s);
            }
            catch (Exception e)
            {
                debug("Error: u64: Base64 conversion error: " + e.Message);
            }
            return b;
        }
    }
}
